
#pragma once

#include <cassert>

#include "vec3.h"
#include "Vec4.h"


class mat4
{
public:
    float m[16];
    
    mat4();
	mat4(const float val);
    mat4(const mat4& mat);
	mat4(float mat[16]);
    ~mat4();
    
    static mat4 getOrtho(float left, float right, float bottom, float top, float nearZ, float farZ);
	static mat4 getPerspective(float fovyRadians, float aspect, float nearZ, float farZ);
	static mat4 getLookAt(float eyeX, float eyeY, float eyeZ,
						  float centerX, float centerY, float centerZ,
						  float upX, float upY, float upZ);
	static mat4 getIdentity();
	static mat4 getRotated(float radians, float x, float y, float z);
	static mat4 getRotatedX(float radians);
	static mat4 getRotatedY(float radians);
	static mat4 getRotatedZ(float radians);
    
	void translateX(const float dx);
	void translateY(const float dy);
	void translateZ(const float dz);
	void translate(const float dx, const float dy, const float dz);
	void rotate(float radians, float x, float y, float z);
	void rotateX(float radians);
	void rotateY(float radians);
	void rotateZ(float radians);
    void scale(float dxSc, float dySc, float dzSc);

    float&		 operator [] (const unsigned int index);
	const float& operator [] (const unsigned int index) const;
	mat4&		 operator =  (const mat4& mat);
    mat4&		 operator -  ();
    
    friend bool		   operator == (const mat4& left, const mat4& right);
    friend mat4&	   operator += (mat4& left, const mat4& right);
    friend mat4&	   operator -= (mat4& left, const mat4& right);
	friend mat4&	   operator *= (mat4& left, const float right);
	friend mat4&	   operator *= (mat4& left, const mat4& right);
    friend const mat4  operator +  (const mat4& left, const mat4& right);
    friend const mat4  operator -  (const mat4& left, const mat4& right);
    friend const mat4  operator *  (const mat4& left, const float right);
    friend const mat4  operator *  (const mat4& left, const mat4& right);
	friend const vec4  operator *  (const mat4& left, const vec4& right);
};




inline mat4::mat4()
{
	m[0]  = 0; m[1]  = 0; m[2]  = 0; m[3]  = 0;
	m[4]  = 0; m[5]  = 0; m[6]  = 0; m[7]  = 0;
	m[8]  = 0; m[9]  = 0; m[10] = 0; m[11] = 0;
	m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 0;
}

inline mat4::mat4(const float val)
{
	m[0]  = val; m[1]  = val; m[2]  = val; m[3]  = val;
	m[4]  = val; m[5]  = val; m[6]  = val; m[7]  = val;
	m[8]  = val; m[9]  = val; m[10] = val; m[11] = val;
	m[12] = val; m[13] = val; m[14] = val; m[15] = val;
}

inline mat4::mat4(const mat4& mat)
{
	m[0]  = mat[0];  m[1]  = mat[1];  m[2]  = mat[2];  m[3]  = mat[3];
	m[4]  = mat[4];  m[5]  = mat[5];  m[6]  = mat[6];  m[7]  = mat[7];
	m[8]  = mat[8];  m[9]  = mat[9];  m[10] = mat[10]; m[11] = mat[11];
	m[12] = mat[12]; m[13] = mat[13]; m[14] = mat[14]; m[15] = mat[15];
}

inline mat4::mat4(float mat[16])
{
	m[0]  = mat[0];  m[1]  = mat[1];  m[2]  = mat[2];  m[3]  = mat[3];
	m[4]  = mat[4];  m[5]  = mat[5];  m[6]  = mat[6];  m[7]  = mat[7];
	m[8]  = mat[8];  m[9]  = mat[9];  m[10] = mat[10]; m[11] = mat[11];
	m[12] = mat[12]; m[13] = mat[13]; m[14] = mat[14]; m[15] = mat[15];
}

inline mat4::~mat4() 
{

}

inline mat4 mat4::getOrtho(float left, float right, float bottom, float top, float nearZ, float farZ)
{
    float ral = right + left;
    float rsl = right - left;
    float tab = top + bottom;
    float tsb = top - bottom;
    float fan = farZ + nearZ;
    float fsn = farZ - nearZ;
    
    mat4 res;
    
    res[0]  = 2.0f / rsl;
    res[5]  = 2.0f / tsb;
    res[10] = -2.0f / fsn;
    res[12] = -ral / rsl;
    res[13] = -tab / tsb;
    res[14] = -fan / fsn;
    res[15] = 1.0f;
    
    return res;
};

inline mat4 mat4::getPerspective(float fovyRadians, float aspect, float nearZ, float farZ)
{
    float cotan = 1.0f / tanf(fovyRadians / 2.0f);
    
    mat4 res;
	res[0] = cotan / aspect;
	res[1] = 0.0f;
	res[2] = 0.0f; 
	res[3] = 0.0f;
                     
	res[4] = 0.0f; 
	res[5] = cotan; 
	res[6] = 0.0f;
	res[7] = 0.0f;
                     
	res[8] = 0.0f; 
	res[9] = 0.0f; 
	res[10] = (farZ + nearZ) / (nearZ - farZ); 
	res[11] = -1.0f;
                     
	res[12] = 0.0f;
	res[13] = 0.0f; 
	res[14] = (2.0f * farZ * nearZ) / (nearZ - farZ); 
	res[15] = 0.0f;
    
    return res;
	

	/*
	mat4 res;

	float range = tanf(fovyRadians / 2.0f) * nearZ;	
	float left = -range * aspect;
	float right = range * aspect;
	float bottom = -range;
	float top = range;

	res[0] = (2.0f * nearZ) / (right - left);
	res[5] = (2.0f * nearZ) / (top - bottom);
	res[10] = - (farZ + nearZ) / (farZ - nearZ);
	res[11] = - 1.0f;
	res[14] = - (2.0f * farZ * nearZ) / (farZ - nearZ);
	
	
	return res;
	*/
}

inline mat4 mat4::getLookAt(float eyeX, float eyeY, float eyeZ,
						    float centerX, float centerY, float centerZ,
						    float upX, float upY, float upZ)
{
	vec3 ev(eyeX, eyeY, eyeZ);
    vec3 cv(centerX, centerY, centerZ);
    vec3 uv(upX, upY, upZ);
	vec3 n = ev - cv; n.normalize();
	vec3 u = uv ^ n; u.normalize();
    vec3 v = n ^ u;
    
    mat4 m;
	
	m[0] = u.x;
	m[1] = v.x;
	m[2] = n.x; 
	m[3] = 0.0f;
    m[4] = u.y;
	m[5] = v.y; 
	m[6] = n.y; 
	m[7] = 0.0f;
    m[8] = u.z;
	m[9] = v.z; 
	m[10] = n.z; 
	m[11] = 0.0f;
    m[12] = (-u) * ev;
    m[13] = (-v) * ev;
    m[14] = (-n) * ev;
    m[15] = 1.0f;
    
    return m;
}

inline mat4 mat4::getIdentity()
{
	mat4 res;

	res[0]  = 1.0f;
	res[5]  = 1.0f;
	res[10] = 1.0f;
	res[15] = 1.0f;

	return res;
}

inline mat4 mat4::getRotated(float radians, float x, float y, float z)
{
	vec3 v(x, y, z);
	v.normalize();
    float cos = cosf(radians);
    float cosp = 1.0f - cos;
    float sin = sinf(radians);
    
	mat4 m;

    m[0] = cos + cosp * v.x * v.x;
    m[1] = cosp * v.x * v.y + v.z * sin;
    m[2] = cosp * v.x * v.z - v.y * sin;
    m[3] = 0.0f;
    m[4] = cosp * v.x * v.y - v.z * sin;
    m[5] = cos + cosp * v.y * v.y;
    m[6] = cosp * v.y * v.z + v.x * sin;
    m[7] = 0.0f;
    m[8] = cosp * v.x * v.z + v.y * sin;
    m[9] = cosp * v.y * v.z - v.x * sin;
    m[10] = cos + cosp * v.z * v.z;
    m[11] = 0.0f;
    m[12] = 0.0f;
    m[13] = 0.0f;
    m[14] = 0.0f;
    m[15] = 1.0f;

	return m;
}

inline mat4 mat4::getRotatedX(float radians)
{
	float cos = cosf(radians);
    float sin = sinf(radians);
    
    mat4 m;

	m[0]  = 1.0f; m[1]  = 0.0f; m[2]  = 0.0f; m[3]  = 0.0f;
    m[4]  = 0.0f; m[5]  = cos;  m[6]  = sin;  m[7]  = 0.0f;
    m[8]  = 0.0f; m[9]  = -sin; m[10] = cos;  m[11] = 0.0f;
    m[12] = 0.0f; m[13] = 0.0f; m[14] = 0.0f; m[15] = 1.0f;
    
    return m;
}

inline mat4 mat4::getRotatedY(float radians)
{
	float cos = cosf(radians);
    float sin = sinf(radians);
    
    mat4 m;

	m[0]  = cos;  m[1]  = 0.0f; m[2]  = -sin; m[3]  = 0.0f;
    m[4]  = 0.0f; m[5]  = 1.0f; m[6]  = 0.0f; m[7]  = 0.0f;
    m[8]  = sin;  m[9]  = 0.0f; m[10] = cos;  m[11] = 0.0f;
    m[12] = 0.0f; m[13] = 0.0f; m[14] = 0.0f; m[15] = 1.0f;

    return m;
}

inline mat4 mat4::getRotatedZ(float radians)
{
	float cos = cosf(radians);
    float sin = sinf(radians);
    
    mat4 m;

	m[0]  = cos;  m[1]  = sin;  m[2]  = 0.0f; m[3]  = 0.0f;
    m[4]  = -sin; m[5]  = cos;  m[6]  = 0.0f; m[7]  = 0.0f;
    m[8]  = 0.0f; m[9]  = 0.0f; m[10] = 1.0f; m[11] = 0.0f;
    m[12] = 0.0f; m[13] = 0.0f; m[14] = 0.0f; m[15] = 1.0f;

    return m;
}

inline void mat4::translateX(const float dx)
{
	m[12] += m[0] * dx;
	m[13] += m[1] * dx;
	m[14] += m[2] * dx;
}

inline void mat4::translateY(const float dy)
{
	m[12] += m[4] * dy;
	m[13] += m[5] * dy;
	m[14] += m[6] * dy;
}

inline void mat4::translateZ(const float dz)
{
	m[12] += m[8]  * dz;
	m[13] += m[9]  * dz;
	m[14] += m[10] * dz;
}

inline void mat4::translate(const float dx, const float dy, const float dz)
{
	m[12] += m[0] * dx + m[4] * dy + m[8]  * dz;
	m[13] += m[1] * dx + m[5] * dy + m[9]  * dz;
	m[14] += m[2] * dx + m[6] * dy + m[10] * dz;
}

inline void mat4::rotate(float radians, float x, float y, float z)
{
	*this *= mat4::getRotated(radians, x, y, z);
}

inline void mat4::rotateX(float radians)
{
	*this *= mat4::getRotatedX(radians);
}

inline void mat4::rotateY(float radians)
{
	*this *= mat4::getRotatedY(radians);
}

inline void mat4::rotateZ(float radians)
{
	*this *= mat4::getRotatedZ(radians);
}

inline void mat4::scale(float dxSc, float dySc, float dzSc)
{
	m[0] *= dxSc;  m[1] *= dxSc;  m[2]  *= dxSc;  m[3]  *= dxSc;
	m[4] *= dySc;  m[5] *= dySc;  m[6]  *= dySc;  m[7]  *= dySc;
	m[8] *= dzSc;  m[9] *= dzSc;  m[10] *= dzSc;  m[11] *= dzSc;
}

inline float& mat4::operator [] (const unsigned int index)
{
	return m[index];
}

inline const float& mat4::operator [] (const unsigned int index) const
{
	return m[index];
}

inline mat4& mat4::operator = (const mat4& mat)
{
	for (int i = 0; i < 16; i++) {
		m[i] = mat[i];
	}

	return *this;
}

inline mat4& mat4::operator - ()
{
	for (int i = 0; i < 16; i++) {
		m[i] *= -1;
	}

	return *this;
}
    
inline bool	operator == (const mat4& left, const mat4& right)
{
	for (int i = 0; i < 16; i++) {
		if (left[i] != right[i]) {
			return false;
		}
	}

	return true;
}

inline mat4& operator += (mat4& left, const mat4& right)
{
	for (int i = 0; i < 16; i++) {
		left[i] += right[i];
	}

	return left;
}
    
inline mat4& operator -= (mat4& left, const mat4& right)
{
	for (int i = 0; i < 16; i++) {
		left[i] -= right[i];
	}

	return left;
}
	
inline mat4& operator *= (mat4& left, const float right)
{
	for (int i = 0; i < 16; i++) {
		left[i] *= right;
	}

	return left;
}
    
inline mat4& operator *= (mat4& left, const mat4& right)
{
	mat4 newLeft = left * right;

	left = newLeft;
    
    return left;
}

inline const mat4 operator + (const mat4& left, const mat4& right)
{
	mat4 res;

	for (int i = 0; i < 16; i++) {
		res[i] = left[i] + right[i];
	}

	return res;
}
    
inline const mat4 operator - (const mat4& left, const mat4& right)
{
	mat4 res;

	for (int i = 0; i < 16; i++) {
		res[i] = left[i] - right[i];
	}

	return res;
}
    
inline const mat4 operator * (const mat4& left, const float right)
{
	mat4 res;

	for (int i = 0; i < 16; i++) {
		res[i] = left[i] * right;
	}

	return res;
}
    
inline const mat4 operator * (const mat4& left, const mat4& right)
{
	mat4 res;
    
    res[0]  = left[0] * right[0]  + left[4] * right[1]  + left[8] * right[2]   + left[12] * right[3];
	res[4]  = left[0] * right[4]  + left[4] * right[5]  + left[8] * right[6]   + left[12] * right[7];
	res[8]  = left[0] * right[8]  + left[4] * right[9]  + left[8] * right[10]  + left[12] * right[11];
	res[12] = left[0] * right[12] + left[4] * right[13] + left[8] * right[14]  + left[12] * right[15];
    
	res[1]  = left[1] * right[0]  + left[5] * right[1]  + left[9] * right[2]   + left[13] * right[3];
	res[5]  = left[1] * right[4]  + left[5] * right[5]  + left[9] * right[6]   + left[13] * right[7];
	res[9]  = left[1] * right[8]  + left[5] * right[9]  + left[9] * right[10]  + left[13] * right[11];
	res[13] = left[1] * right[12] + left[5] * right[13] + left[9] * right[14]  + left[13] * right[15];
    
	res[2]  = left[2] * right[0]  + left[6] * right[1]  + left[10] * right[2]  + left[14] * right[3];
	res[6]  = left[2] * right[4]  + left[6] * right[5]  + left[10] * right[6]  + left[14] * right[7];
	res[10] = left[2] * right[8]  + left[6] * right[9]  + left[10] * right[10] + left[14] * right[11];
	res[14] = left[2] * right[12] + left[6] * right[13] + left[10] * right[14] + left[14] * right[15];
    
	res[3]  = left[3] * right[0]  + left[7] * right[1]  + left[11] * right[2]  + left[15] * right[3];
	res[7]  = left[3] * right[4]  + left[7] * right[5]  + left[11] * right[6]  + left[15] * right[7];
	res[11] = left[3] * right[8]  + left[7] * right[9]  + left[11] * right[10] + left[15] * right[11];
	res[15] = left[3] * right[12] + left[7] * right[13] + left[11] * right[14] + left[15] * right[15];
    
    return res;
}

inline const vec4 operator * (const mat4& left, const vec4& right)
{
	vec4 v( left[0] * right.x + left[4] * right.y + left[8]  * right.z + left[12] * right.w,
            left[1] * right.x + left[5] * right.y + left[9]  * right.z + left[13] * right.w,
            left[2] * right.x + left[6] * right.y + left[10] * right.z + left[14] * right.w,
            left[3] * right.x + left[7] * right.y + left[11] * right.z + left[15] * right.w );

    return v;
}



